<?PHP if( ! defined('BASEPATH')) exit('Sorry, no direct script allowed');

class Restful extends CI_Controller
{
	// set in controller for default rest format
	protected $rest_format = NULL;
	// contains list of methods, such as limit, log, and level
	protected $method = array();
	// stores accept, language, body, headers, etc
	protected $request = NULL;
	// output from sent request 
	protected $response = NULL;
	// stores db, keys, key level, etc
	protected $rest = NULL;
	
	private $_get_args = array();
	private $_post_args = array();
	private $_put_args = array();
	private $_delete_args = array();
	
	private $_args = NULL;
	private $_allow = TRUE;
	
	private $_supported_formats = array
	(
		'xml' => 'application/xml',
		'rawxml' => 'application/xml',
		'json' => 'application/json',
		'jsonp' => 'application/javascript',
		'serialize' => 'application/vnd.php.serialized',
		'php' => 'text/plain',
		'html' => 'text/html',
		'csv' => 'application/csv'
	);
	
	// class contructor declaration
	public function __construct()
	{
		parent::__construct();
		
		// load our configuration
		$this->load->config('rest');
		
		// detect request method
		$this->request->method = $this->_detect_method();
		
		// set up GET variables
		$this->_get_args = array_merge($this->_get_args, $this->uri->ruri_to_assoc(3));
		
		$this->load->library('security');
		
		$this->load->library('format');
		
		$this->request->format = $this->_detect_input_format();
		
		$this->request->body = NULL;
		
		switch($this->request->method)
		{
			case 'get':
				$tamp = array_keys($this->uri->ruri_to_assoc(3));
				parse_str(parse_url($this->input->server('REQUEST_URI'), PHP_URL_QUERY), $get);
				$this->_get_args = array_merge($tamp,$get);
				break;
				
			case 'put':
				if($this->request->format)
				{
					$this->request->body = file_get_contents('php://input');
				}
				else
				{
					parse_str(file_get_contents('php://input'), $this->_put_args);
				}
				
				break;
				
			case 'post':
				$tamp = array_keys($this->uri->ruri_to_assoc(3));
				
				$this->_post_args = array_merge($tamp, $_POST);
				
				$this->request->format && $this->request->body = file_get_contents('php://input');
				
				break;
				
			case 'delete':
				parse_str(file_get_contents('php://input'), $this->_delete_args);
				
				break;	
		}
		
		if($this->request->format && $this->request->body)
		{
			$this->request->body = $this->format->factory($this->request->body, $this->request->format)->to_array();
		}
		
				
		$this->_args = array_merge($this->_put_args, $this->_get_args, $this->_post_args, $this->_delete_args);
		
		$this->response->format = $this->_detect_output_format();
		
		$this->response->lang = $this->_detect_lang();
		
		$this->auth_override = $this->_auth_override_check();
		
		if($this->auth_override === 'none')
		{
			if($this->config->item('rest_auth') === 'basic')
			{
				$this->_prepare_basic_auth();
			}
			elseif($this->config->item('rest_auth') === 'digest')
			{
				$this->_prepare_digest_auth();
			}
		}
		
		if(config_item('rest_enable_keys'))
		{
			$this->_allow = $this->_detect_api_key();
		}
		
		if( ! $this->input->is_ajax_request() && config_item('rest_ajax_only'))
		{
			$this->response(array('status' => FALSE, 'error' => 'Only AJAX requests are accepted'), 505);
		}
		
	}
	
	// main function for handle controller and remap to REST API handler
	public function _remap($object_called, $arguments)
	{
		$pattern = '/^(.*)\.(' . implode('|', array_keys($this->_supported_formats)) .')$/';
		
		if(preg_match($pattern, $object_called, $matches))
		{
			$object_called = $matches[1];
		}
		
		$controller_method = $object_called . '_' . $this->request->method;
		
		$log_method = ! (isset($this->methods[$controller_method]['log']) && $this->method[$controller_method]['log'] === FALSE);
		
		$use_key = ! (isset($this->methods[$controller_method]['key']) && $this->method[$controller_method]['key'] === FALSE);
		
		if(config_item('rest_enable_keys') && $use_key && $this->_allow === FALSE)
		{
			$this->response(array('status' => FALSE, 'error' => 'Invalid API Key'),403);
		}
		
		if( ! method_exists($this, $controller_method))
		{
			$this->response(array('status' => FALSE, 'error' => 'Unknown method'), 404);
		}
		
		if(config_item('rest_enable_keys') && ! empty($this->rest->key))
		{
			// check the limit
			if(config_item('rest_enable_limits') && ! $this->_check_limit($controller_method))
			{
				$this->response(array('status' => FALSE, 'error' => 'This API key has reached the hourly limit for this method'), 401);
			}
			
			$level = isset($this->methods[$controller_method]['level'])? $this->methods[$controller_method]['level'] : 0;
			
			$authorized = $level <= $this->rest->level;
			
			if(config_item('rest_enable_logging') && $log_method)
			{
				$this->_log_request($authorized);
			}
			
			$authorized OR $this->response(array('status' => FALSE, 'error' => 'This API key does not have enough permissions.') , 401);
		}
		elseif(config_item('rest_enable_logging') && $log_method)
		{
			$this->_log_request($authorized = TRUE);
		}
		
		call_user_func_array(array($this, $controller_method), $arguments);
		
	}
	
	public function response($data = array(), $http_code = NULL, $output='')
	{
		if(empty($data) && $http_code === NULL)
		{
			$http_code = '404';
		}
		else
		{
			is_numeric($http_code) OR $http_code = '200';
			
			if(method_exists($this, '_format_'.$this->response->format))
			{
				header('Content-Type: '.$this->_supported_formats[$this->response->format]);
				
				$output = $this->{'_format_'.$this->response->format}($data);
			}
			elseif(method_exists($this->format, 'to_'.$this->response->format))
			{
				header('Content-Type: '.$this->_supported_formats[$this->response->format]);
				
				$output = $this->format->factory($data)->{'to_'.$this->response->format}();
			}
			
		}
		
		header('HTPP/1.1: ' . $http_code);
		header('Status: ' . $http_code);
		header('Content-Length: ' . strlen($output));
		
		exit($output);
		
	}
	
	// INPUT_SECTION HANDLER
	
	public function get($key = NULL, $xss_clean = TRUE)
	{
		if($key === NULL)
		{
			return $this->_get_args;
		}
		
		return array_key_exists($key, $this->_get_args)? $this->_xss_clean($this->_get_args[$key], $xss_clean) : FALSE;
	}
	
	public function post($key = NULL, $xss_clean = TRUE)
	{
		if($key === NULL)
		{
			return $this->_post_args;
		}
		
		return $this->input->post($key, $xss_clean);
	}
	
	public function put($key = NULL, $xss_clean = TRUE)
	{
		if($key === NULL)
		{
			return $this->_put_args;
		}
		
		return array_key_exists($key, $this->_put_args)? $this->_xss_clean($this->_put_args[$key],$xss_clean) : FALSE;
	}
	
	public function delete($key = NULL, $xss_clean = TRUE)
	{
		if($key === NULL)
		{
			return $this->_delete_args;
		}
		
		return array_key_exists($key, $this->_delete_args)? $this->_xss_clean($this->_delete_args[$key],$xss_clean) : FALSE;
	}
	
	private function _xss_clean($val, $bool)
	{
		if (CI_VERSION < 2)
		{
			return $bool ? $this->input->xss_clean($val) : $val;
		}
		else
		{
			return $bool ? $this->security->xss_clean($val) : $val;
		}
	}
	
	public function validation_errors()
	{
		$str = strip_tags($this->form_validation->error_string());
		
		return explode("\n", trim($str,"\n"));
	}
	
	
	// AUTH_SECTION HANDLER

	private function _auth_override_check()
	{
		// get from rest config
		$this->overrides_array = $this->config->item('auth_override_class_method');
		
		// check to see if the override array is populated, if not, return false
		if(empty($this->overrides_array))
		{
			return 'none';
		}
		
		if(array_key_exists($this->uri->uri_string(),$this->overrides_array))
		{
		
		// if none auth override found, return true (means none auth is being used)
		if($this->overrides_array[$this->uri->uri_string()] === 'basic')
		{
			$this->_prepare_basic_auth();
			return 'auth';
		}
		
		if($this->overrides_array[$this->uri->uri_string()] === 'digest')
		{
			$this->_prepare_digest_auth();
			return 'auth';
		}
		}
		// if any check not satisfied (overrides value doesn't match none, basic, or digest)
		return 'none';
	}
	
	
	
	private function _prepare_basic_auth()
	{
		$username = NULL;
		$passwd = NULL;
		
		if($this->input->server('PHP_AUTH_USER'))
		{
			$username = $this->input->server('PHP_AUTH_USER');
			$passwd = $this->input->server('PHP_AUTH_PW');
		}
		elseif($this->input->server('HTTP_AUTHENTICATION'))
		{
			if(strpos(strtolower($this->input->server('HTTP_AUTHENTICATION')),'basic') === 0)
			{
				list($username, $passwd) = explode(':', base64_decode(substr($this->input->server('HTTP_AUTHENTICATION'),6)));
			}
		}
		
		if( ! $this->_check_login($username, $passwd))
		{
			$this->_force_login();
		}
		
	}
	
	private function _prepare_digest_auth()
	{
		$uniqid = uniqid("");
		
		if($this->input->server('PHP_AUTH_DIGEST'))
		{
			$digest_string = $this->input->server('PHP_AUTH_DIGEST');
		}
		elseif($this->input->server('HTTP_AUTHORIZATION'))
		{
			$digest_string = $this->input->server('HTTP_AUTHORIZATION');
		}
		else
		{
			$digest_string = "";
		}
		
		if(empty($digest_string))
		{
			$this->_force_login($uniqid);
		}
		
		preg_match_all('@(username|nonce|uri|nc|cnonce|qop|response)=[\'"]?([\'",]+)@', $digest_string, $matches);
		
		$digest = array_combine($matches[1],$matches[2]);
		
		if( ! array_key_exists('username', $digest) OR  ! $this->_check_login($digest['username']))
		{
			$this->_force_login($uniqid);
		}
		
		$valid_logins = & $this->config->item('rest_valid_logins');
		
		$valid_pass = $valid_logins[$digest['username']];
		
		$A1 = md5($digest['username'] . ':' . $this->config->item('rest_realm') . ':' . $valid_pass);
		
		$A2 = md5(strtoupper($this->request->method) . ':' . $digest['uri']);
		
		$valid_response = md5($A1 . ':' . $digest['nonce'] . ':' . $digest['nc'] . ':' . $digest['cnonce'] . ':' . $digest['qop'] . $A2);
		
		if($digest['response'] !== $valid_response)
		{
			header('HTPP/1.0 401 Unauthorized');
			header('HTPP/1.1 401 Unauthorized');
			
			exit;
		}
	}
	
	private function _check_login($username='', $passwd = NULL)
	{
		if(empty($username))
		{
			return FALSE;
		}
		
		$valid_logins = $this->config->item('rest_valid_logins');
		
		if( ! array_key_exists($username, $valid_logins))
		{
			return FALSE;
		}
		
		// wrong password as defined		
		if($passwd !== NULL && $valid_logins[$username] !== $passwd)
		{
			return FALSE;
		}
		
		return TRUE;
		
	}
	
	private function _force_login($nonce = '')
	{
		if ($this->config->item('rest_auth') === "basic")
		{
			header('WWW-Authenticate: Basic realm="' . $this->config->item('rest_realm') . '"' );
		}
		elseif ($this->config->item('rest_auth') === 'digest')
		{
			header('WWW-Authenticate: Digest realm="' . $this->config->item('rest_realm') . ' " qop="auth" nonce="' . $nonce . '" opaque="' . md5($this->config->item('rest_realm')) . '"' );
		}
		
		$this->response(array("status" => FALSE, "error" => "Not authorized"), '401');
	}
	
	
	// QUOTA REQUEST HANDLER
	private function _check_request_limit($controller_method)
	{
		if( ! empty($this->rest->ignore_limits) OR ! isset($this->methods[$controller_method]['limit']) )
		{
			return TRUE;
		}
		
		$limit = $this->methods[$controller_method]['limit'];
		
		$result = $this->rest->db->
				where('uri', $this->uri->uri_string())->
				where('api_key', $this->rest->key)->
				get(config_item('rest_limits_table'))->
				row();
				
		if( ! $result OR $result->hour_started < time() -(60*60) )
		{
			$this->rest->db->insert(config_item('rest_limits_table', array(
				'uri' => $this->uri->uri_string(),
				'api_key' => $this->rest->key ? $this->rest->key : '',
				'count' => 1,
				'hour_started' => time(),
			)));
		}
		else
		{
			if($result->count > $limit)
			{
				return FALSE;
			}
			
			$this->rest->db->
				where('uri', $this->uri->uri_string())->
				where('api_key', $this->rest->key)->
				set('count', 'count+1', FALSE)->
				update(config_item('rest_limits_table'));
		}
		
		return TRUE;
	}
	
	// FUNCTION_SECTION CLASS HELPER
	
	private function _detect_input_format()
	{
		if($this->input->server('CONTENT_TYPE'))
		{
			foreach($this->_supported_formats as $format => $mime)
			{
				if($this->input->server('CONTENT_TYPE') === $mime)
				{
					return $format;
				}
			}
		}
		
		return NULL;
	}
	
	private function _detect_output_format()
	{
		$pattern = '/\.(' . implode('|', array_keys($this->_supported_formats)) .')$/';
		
		if(preg_match($pattern, $this->uri->uri_string(), $matches))
		{
			return $matches[1];
		}
		elseif($this->_get_args && preg_match($pattern, end($this->_get_args), $matches))
		{
			$last_key = end(array_keys($this->_get_args));
			
			$this->_get_args[$last_key] = preg_replace($pattern, '', $this->_get_args[$last_key]);
			$this->_args[$last_key] = preg_replace($pattern, '', $this->_args[$last_key]);
			
			return $matches[1]; 
		}
		
		// if format is provided on url and it is supported
		if(isset($this->_get_args['format']) && array_key_exists($this->_get_args['format'], $this->_supported_formats) )
		{
			return $this->_get_args['format'];
		}
		
		// otherwise, check the HTPP_ACCEPT (if it exists and we are allowed)
		if($this->config->item('rest_ignore_http_accept') === FALSE && $this->input->server('HTTP_ACCEPT'))
		{
			// check all supported formats agains the HTTP_ACCEPT header
			foreach(array_keys($this->_supported_formats) as $format)
			{
				if(strpos($this->input->server('HTTP_ACCEPT'), $format) !== FALSE)
				{
					// if not HTML or XML
					if($format !== 'html' && $format !== 'xml')
					{
						return $format;
					}
					else
					{
						if($format === 'html' && strpos($this->input->server('HTTP_ACCEPT'),'xml') === FALSE)
						{
							return $format;
						}
						elseif($format === 'xml' && strpos($this->input->server('HTTP_ACCEPT'),'html') === FALSE)
						{
							return $format;
						}
					}
				}
			}
		}
		
		if( ! empty($this->rest_format))
		{
			return $this->rest_format;
		}
		
		return config_item('rest_default_format');
	}
	
	private function _detect_api_key()
	{
		$key_name = "HTTP_" . strtoupper(str_replace('-', '_', config_item('rest_key_name')));
		
		$this->rest->key = NULL;
		$this->rest->level = NULL;
		$this->rest->ignore_limits = FALSE;
		
		if($key = isset($this->_args['API-Key'])? $this->_args['API-Key']: $this->input->server($key_name) )
		{
			if( ! $row = $this->rest->db->where('key', $key)->get(config_item('rest_keys_table'))->row())
			{
				return FALSE;
			}
			
			$this->rest->key = $row->key;
			$this->rest->level = $row->level;
			$this->rest->ignore_limits = $row->ignore_limits;
			
			return TRUE;
			
		}
		
		// if no API key is provided
		return FALSE;
	}
	
	private function _detect_lang()
	{
		if( ! $lang = $this->input->server('HTTP_ACCEPT_LANGUAGE'))
		{
			return NULL;
		}
		
		if(strpos($lang, ',') !== FALSE)
		{
			$langs = explode(',', $lang);
			
			$return_langs = array();
			
			$i = 1;
			
			foreach($langs as $lang)
			{
				list($lang) = explode(';', $lang);
				
				$return_langs[] = trim($lang);
			}
			
			return $return_langs;
		}
		
		return $lang;
		
	}
	
	function _get_connected()
	{
		$config['dbdriver'] = "sqlite";
		$config['rest_db'] = BASEPATH . 'dbdata/fluks.db';
		
		$this->mydb = new PDO("sqlite:" . $config['rest_db']);
		$this->mydb->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION ); 
	}
	
	
	function _get_disconnected()
	{
		$this->mydb = NULL;
	}
	
	private function _log_request($authorized = FALSE)
	{
		$this->_get_connected();
		
		$auto_id = time() . $this->input->ip_address();
		$s = '';
		
		$params = array(
			$auto_id,
			$this->uri->uri_string(),
			$this->request->method,
			implode('/',$this->_args),
			isset($this->rest->key)? $this->rest->key : '',
			$this->input->ip_address(),
			function_exists('now')? now() : time(),
			$authorized
		);
		
		$s = 'INSERT INTO '. config_item('rest_logs_table') .' (id,uri,method,params,api_key,ip_address,time_log,authorized) VALUES (?,?,?,?,?, ?,?,?)';
		
		try
		{
			$tmd = $this->mydb->prepare($s);
			$tmd->execute($params);
		}
		catch(PDOException $e)
		{
			$this->_get_disconnected();
			$output = array("Status" => "ERROR","Message" => $e->getMessage());
			file_put_contents('PDOErrors.txt', 
				'['. date('d.m.y H.i.s') .']: '. $e->getMessage() . '
', FILE_APPEND);
			
			return FALSE;
		}
		
		$this->_get_disconnected();
		
		return TRUE;
	}
	
	// detecting HTTP method, GET, PUT, POST, DELETE
	private function _detect_method()
	{
		// Check method from inputted server
		$method = strtolower($this->input->server('REQUEST_METHOD'));
		
		// Check config if there is 'fake' request for trial, if so, using it
		if ($this->config->item('enable_emulate_request') && $this->input->post('_method'))
		{
			$method = $this->input->post('_method');
		}
		
		// Finally, check if there is in HTTP method array
		if (in_array($method, array('get','post','put','delete')))
		{
			return $method; 
		}
		
		// if any method not found, just using get
		return "get";
	}
	
	// force data into an array
	private function _force_loopable($data)
	{
		if ( ! is_array($data) && ! is_object($data) )
		{
			$data = (array) $data;
		}
		
		return $data;
	}
	
	// encode format as JSONP - Javascript Object
	private function _format_jsonp($data = array())
	{
		return $this->get('callback') . '(' . json_encode($data) . ')'; 
	}
}
